Skip to content

feat(organizations): owner_add lifecycle + addMember invitee email#3858

Merged
PierreBrisorgueil merged 6 commits into
masterfrom
fix/3831-3832-owner-add-lifecycle
Jun 13, 2026
Merged

feat(organizations): owner_add lifecycle + addMember invitee email#3858
PierreBrisorgueil merged 6 commits into
masterfrom
fix/3831-3832-owner-add-lifecycle

Conversation

@PierreBrisorgueil

@PierreBrisorgueil PierreBrisorgueil commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Summary

Node half of the owner_add membership lifecycle (cross-stack pair — the Vue surfaces land in a follow-up PR that closes these issues).

#3831 — owner_add lifecycle:

  • Decline: DELETE /api/membership-requests/:membershipId (auth-only) + MembershipService.declineMembership() — consent gate identical to acceptMembership (pending + owner_add + caller is the invitee), opaque 404 on any mismatch. The "Please accept or decline it" copy is finally honest.
  • Last-owner scope fix: remove()'s last-owner protection now only fires for status === ACTIVE owner rows — cancelling a pending owner-invite in a 1-active-owner org no longer spuriously throws.
  • Pending visibility: the members list now returns pending owner_add rows ($or of ACTIVE + PENDING/owner_add); pending join_requests stay on their own approval surface.
  • User-delete cleanup: users.service remove() now sweeps the deleted user's PENDING membership rows (both sources) so they don't orphan.

#3832 — invitee email: addMember() sends a fire-and-forget invitation email (new config/templates/org-member-added.html, pending-acceptance wording — never "you were added"), mailer.isConfigured-gated, .catch(logger.warn), never failing the add. (The cap-error wording half of #3832 is frontend-only — it ships with the Vue PR.)

Test plan

5 new suites: decline e2e (consent gate), membership lifecycle unit, addMember email unit, silent-catch unit, user-delete pending-sweep unit. Full organizations.membership set: 8 suites / 86 tests green. Lint clean.

Guardrails

  • npm run lint clean
  • Public-OSS clean (no downstream names/domains/infra/board-ids)
  • Fire-and-forget mailer never throws the business path

refs #3831
refs #3832

Summary by CodeRabbit

  • New Features

    • Invited users can now decline organization membership invitations.
    • Organization owners receive email notifications when adding new members.
    • Owners can view pending member invitations in the members list.
  • Bug Fixes

    • Pending memberships are now properly cleaned up when users are removed.

declineMembership mirrors acceptMembership's consent gate (pending +
owner_add + caller is the invitee) and deletes the row so the user can
be re-invited. remove() now runs last-owner protection only for ACTIVE
owner rows — cancelling a pending owner-role invite in a 1-active-owner
org no longer throws. list() surfaces pending owner_add rows next to
active members so the inviting owner can see and cancel an invite;
pending join_requests keep their own approval surface.

refs #3831
remove() iterates listByUser, which is ACTIVE-only, so a deleted
user's PENDING rows (join_request and owner_add) survived as orphans
pointing at a dead userId. Sweep them explicitly after the
active-membership processing.

refs #3831
DELETE /api/membership-requests/:membershipId — auth-only chain like
the accept route; the consent gate lives in the service and answers 404
on any mismatch without leaking which condition failed. Registered
after /mine and /mine/pending so those literals are never captured as
an id. Makes the 'Please accept or decline it' join-request copy
honest, proven end-to-end by the new e2e flow.

refs #3831
addMember created a pending owner_add membership silently while all three
join-request paths email their counterpart — an org-less invitee got zero
signal. Notify the invited user via the new org-member-added template with
invitation wording (the membership stays PENDING until THEY accept, consent
invariant #1), fire-and-forget with logger.warn on failure, guarded by
mailer.isConfigured() like the existing membership emails. refs #3832
Gate the OrganizationRepository.get behind mailer.isConfigured (matches

approveRequest/rejectRequest) — no needless DB read on the mail-off path. refs #3832
Copilot AI review requested due to automatic review settings June 13, 2026 15:41
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@PierreBrisorgueil, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 47 minutes and 2 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 04dadd7c-052c-4e39-996e-659667d5a954

📥 Commits

Reviewing files that changed from the base of the PR and between 09f109f and febc4aa.

📒 Files selected for processing (7)
  • config/templates/org-member-added.html
  • modules/organizations/repositories/organizations.membership.repository.js
  • modules/organizations/services/organizations.membership.service.js
  • modules/organizations/tests/organizations.decline.e2e.tests.js
  • modules/organizations/tests/organizations.membership.addMember.email.unit.tests.js
  • modules/organizations/tests/organizations.membership.lifecycle.unit.tests.js
  • modules/organizations/tests/organizations.membershipRequest.controller.unit.tests.js

Walkthrough

This PR implements the owner_add invitation decline feature. It adds an email template for invitations, updates addMember to send emails, introduces a declineMembership service function with consent gating, wires HTTP endpoints for the decline action, updates the members list filter to show pending invitations, narrows the last-owner protection to ACTIVE members only, sweeps pending memberships on user deletion, and includes comprehensive unit and E2E tests.

Changes

Organization owner_add invitation decline flow

Layer / File(s) Summary
Email template and invitation sending
config/templates/org-member-added.html, modules/organizations/services/organizations.membership.service.js, modules/organizations/tests/organizations.membership.addMember.email.unit.tests.js, modules/organizations/tests/organizations.membership.silent.catch.unit.tests.js
org-member-added.html template greets invitees by name with org/app context and acceptance link. addMember is expanded to send invitation emails when mailer is configured and user has email, using fire-and-forget error handling logged via logger.warn. Unit tests verify email sends in happy path and that missing mailer/email config skips sending without blocking membership creation.
Decline membership service logic
modules/organizations/services/organizations.membership.service.js, modules/organizations/tests/organizations.membership.lifecycle.unit.tests.js
list() filter updated to include PENDING memberships with source OWNER_ADD alongside ACTIVE rows so pending invitations appear in members view. remove() last-owner protection narrowed to check only ACTIVE owner memberships, allowing pending owner invites to be cancelled without protection. New declineMembership(membershipId, decliningUserId) implements invitee-only consent gate and deletes the membership record if matched. Unit tests cover gating logic, rejection of other users, last-owner scoping to ACTIVE, and list filter behavior.
Decline membership HTTP API
modules/organizations/controllers/organizations.membershipRequest.controller.js, modules/organizations/routes/organizations.membershipRequest.routes.js
New decline controller endpoint calls MembershipService.declineMembership and returns 404 with fixed message when no invitation matches. DELETE /api/membership-requests/:membershipId route added with JWT authentication to allow invitees to decline their invitations.
User deletion pending membership cleanup
modules/users/services/users.service.js, modules/users/tests/users.service.remove.pendingSweep.unit.tests.js
remove() performs pending membership sweep (deleteMany({userId, status: PENDING})) before user deletion to prevent orphaned invitations/join requests. Unit tests verify sweep runs with correct filter and that active membership processing does not prevent the pending cleanup from executing once.
End-to-end decline flow validation
modules/organizations/tests/organizations.decline.e2e.tests.js
E2E test bootstraps app and agents for owner and invitee. Owner invites invitee (creating pending OWNER_ADD membership), invitee is visible in members list with status: pending and source: owner_add. Owner's decline attempt returns 404 and does not delete record. Invitee's decline deletes the membership record and returns success. Invitee then creates a join request successfully. Suite restores config and cleans up users/memberships.

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

  • #3831: Main feature implementing owner_add lifecycle (decline endpoint, declineMembership service, last-owner scoping, pending visibility, user cleanup sweep).
  • #3832: Related feature modifying addMember and invitation flow (email template and mailer integration in addMember).

Possibly related PRs

  • pierreb-devkit/Node#3403: Both PRs modify organizations.membership.service.js last-owner/remove logic and membership status handling, main PR via scoping removal check to ACTIVE memberships.
  • pierreb-devkit/Node#3822: Both PRs update organizations membership-request controller/routes surface; main PR adds decline handler for pending invitations, retrieved PR removes legacy email-invite handlers.
  • pierreb-devkit/Node#3366: Both PRs update MembershipService email-failure logging and add test coverage for addMember silent catch behavior in the same audit/logging area.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(organizations): owner_add lifecycle + addMember invitee email' accurately captures the two main features being implemented in this changeset: the owner_add membership lifecycle (including decline, last-owner scope fix, and pending visibility) and the invitee email functionality for addMember.
Description check ✅ Passed The PR description is comprehensive and covers all major template sections: Summary (detailed breakdown of changes), Test plan (5 new suites with counts), and Guardrails (checklist items marked). However, the Scope section is entirely missing and Validation section is not included in the provided description.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/3831-3832-owner-add-lifecycle

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Jun 13, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.59259% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.37%. Comparing base (26dc3ff) to head (febc4aa).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3858      +/-   ##
==========================================
+ Coverage   92.03%   92.37%   +0.33%     
==========================================
  Files         160      160              
  Lines        5337     5361      +24     
  Branches     1717     1723       +6     
==========================================
+ Hits         4912     4952      +40     
+ Misses        337      328       -9     
+ Partials       88       81       -7     
Flag Coverage Δ
integration 59.93% <18.51%> (-0.18%) ⬇️
unit 73.04% <88.88%> (+0.40%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 26dc3ff...febc4aa. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR completes the Node-side of the owner_add membership lifecycle by adding an invitee-decline endpoint, fixing last-owner protection scope, and exposing pending owner_add rows on the members list. It also adds a fire-and-forget invitation email when addMember() creates a pending invite, and ensures user deletion cleans up orphanable pending membership rows.

Changes:

  • Add DELETE /api/membership-requests/:membershipId + MembershipService.declineMembership() with the same consent gate semantics as accept (opaque 404 on mismatch).
  • Adjust membership listing/removal behavior: list includes pending owner_add, and last-owner guard applies only to ACTIVE owners.
  • Add invitee notification email on addMember() (mailer-configured only) and sweep pending memberships during user removal, with comprehensive unit/e2e test coverage.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
modules/users/services/users.service.js Sweeps deleted user’s pending memberships to prevent orphaned pending rows.
modules/users/tests/users.service.remove.pendingSweep.unit.tests.js Unit tests validating pending-membership sweep on user deletion.
modules/organizations/services/organizations.membership.service.js Implements pending visibility, last-owner scope fix, invitation email in addMember, and declineMembership.
modules/organizations/routes/organizations.membershipRequest.routes.js Adds authenticated decline route for pending owner_add invitations.
modules/organizations/controllers/organizations.membershipRequest.controller.js Adds decline controller that returns opaque 404 on any consent mismatch.
modules/organizations/tests/organizations.membership.silent.catch.unit.tests.js Extends silent-catch logging tests to cover addMember email failures.
modules/organizations/tests/organizations.membership.lifecycle.unit.tests.js Unit tests for decline consent gate, last-owner scope, and list filter behavior.
modules/organizations/tests/organizations.membership.addMember.email.unit.tests.js Unit tests for addMember invitation email behavior across configured/unconfigured/no-email cases.
modules/organizations/tests/organizations.decline.e2e.tests.js E2E test covering full decline flow and re-request-to-join behavior.
config/templates/org-member-added.html Adds email template used by the new addMember invitation notification.

Comment thread modules/organizations/services/organizations.membership.service.js

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@config/templates/org-member-added.html`:
- Line 2: The template has an empty <title></title> in org-member-added.html
which fails lint; update the <title> element to a meaningful, non-empty string
(e.g., "You're invited to join {{orgName}}" or another appropriate template
variable) so the HTML title-require rule passes; modify the <title> tag in the
file to include that static text or interpolation and keep any existing head
structure unchanged.

In `@modules/organizations/services/organizations.membership.service.js`:
- Around line 491-500: Current code reads the membership with
MembershipRepository.get and later calls MembershipRepository.remove, which
risks a race where the membership's status changes between read and delete;
replace the two-step read+remove with a single atomic delete constrained by _id
+ userId + status:PENDING + source:OWNER_ADD (use membershipId and
decliningUserId and MEMBERSHIP_STATUSES.PENDING + PENDING_SOURCES.OWNER_ADD) so
the delete only succeeds if the document is still pending-owner-add and returns
the removed document (or null) in one operation (e.g., use a findOneAndDelete /
deleteOne returning document method on MembershipRepository or add an atomic
method to the repository if missing), and then return that result instead of
calling remove after get.

In `@modules/organizations/tests/organizations.decline.e2e.tests.js`:
- Around line 25-36: The JSDoc for the named helper functions is missing
`@returns` tags; update the JSDoc for resetOrgConfig to include an explicit
"`@returns` {void} Resets organizations config to original state." and update the
JSDoc for the async cleanupUser helper to include an explicit "`@returns`
{Promise<void>} Resolves when the user and associated organizations/memberships
are cleaned up." — ensure these tags are added to the existing comment blocks
above the resetOrgConfig and cleanupUser function declarations.

In
`@modules/organizations/tests/organizations.membership.addMember.email.unit.tests.js`:
- Around line 100-117: Add assertions to ensure org DB reads are skipped in the
two tests that verify sendMail is not called: in the test named "mailer NOT
configured: creates the membership and never calls sendMail" and in the test
named "user without an email: creates the membership and never calls sendMail",
add expect(mockOrgGet).not.toHaveBeenCalled() after the existing
expect(mockSendMail).not.toHaveBeenCalled(); this locks in the DB-read guard so
MembershipService.addMember (the function under test) cannot silently call
mockOrgGet when emails are not being sent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4c9a5334-7b95-465e-b1fc-ad0ba5afd353

📥 Commits

Reviewing files that changed from the base of the PR and between 26dc3ff and 09f109f.

📒 Files selected for processing (10)
  • config/templates/org-member-added.html
  • modules/organizations/controllers/organizations.membershipRequest.controller.js
  • modules/organizations/routes/organizations.membershipRequest.routes.js
  • modules/organizations/services/organizations.membership.service.js
  • modules/organizations/tests/organizations.decline.e2e.tests.js
  • modules/organizations/tests/organizations.membership.addMember.email.unit.tests.js
  • modules/organizations/tests/organizations.membership.lifecycle.unit.tests.js
  • modules/organizations/tests/organizations.membership.silent.catch.unit.tests.js
  • modules/users/services/users.service.js
  • modules/users/tests/users.service.remove.pendingSweep.unit.tests.js

Comment thread config/templates/org-member-added.html Outdated
Comment thread modules/organizations/services/organizations.membership.service.js Outdated
Comment thread modules/organizations/tests/organizations.decline.e2e.tests.js
- email template: add non-empty <title>{{appName}} invitation</title>
  (fixes title-require lint error flagged by CodeRabbit/HTMLHint)
- addMember: wrap OrganizationRepository.get() in try/catch so any DB
  error inside the notification block never propagates to the caller;
  the fire-and-forget guarantee now covers the org-fetch step too
  (Copilot finding — line 421)
- declineMembership: replace read-then-delete with atomic
  MembershipRepository.findOneAndDelete({_id, userId, status:PENDING,
  source:OWNER_ADD}) — eliminates the accept/decline race window where a
  row activated between get() and remove() could be wrongly deleted
  (CodeRabbit major finding — line 500); add findOneAndDelete() to repo
- lifecycle unit tests: update declineMembership suite to mock/assert
  the new findOneAndDelete path; add concurrent-accept race scenario
- membershipRequest controller unit tests: add 4 decline() cases
  (happy path, null→404, id fallback, throw→422) to close the 7-line
  patch coverage gap reported by codecov
- addMember email unit tests: add expect(mockOrgGet).not.toHaveBeenCalled()
  to the two skip-email branches (CodeRabbit nit — line 117)
- decline e2e tests: add @returns JSDoc tags to resetOrgConfig and
  cleanupUser named helpers (CodeRabbit nit — line 36)

refs #3831
refs #3832
@PierreBrisorgueil PierreBrisorgueil merged commit 12b961c into master Jun 13, 2026
8 checks passed
@PierreBrisorgueil PierreBrisorgueil deleted the fix/3831-3832-owner-add-lifecycle branch June 13, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants